react 新特性和 Hooks

react 新特性

  • Context 实现跨层级的组件数据传递,静态属性 ContextType 访问跨层级组件的数据
  • Lazy 与 Suspense 实现延迟加载(类似于 vue 的异步组件)
  • Memo 实现指定组件进行渲染(优化组件渲染,当传入组件的属性值都不变的情况下,不会触发组件的重新渲染,用于函数组件中替代 PureComponent,注意 PureComponent 只监听第一层属性的变化,当属性拆分越简单,使用的概率就越高)

这里暂时举例,官网都有,下面使用 Hooks 也会涉及到部分

Hooks

Hooks 的意义

类组件的不足:

  • 状态逻辑难以复用
    • 缺少复用机制
    • 渲染属性和高阶组件会导致层级冗余
  • 趋向复杂难以维护
    • 生命周期函数混杂不相干逻辑
    • 相干逻辑分散在不同生命周期函数中
  • this 指向困扰
    • 内联函数过度创建新句柄
    • 类成员函数不能保证 this

Hooks 的优势:

  • 优化类组件的三大问题
    • 函数组件无 this 问题
    • 自定义 Hook 方便复用状态逻辑
    • 副作用的关注点分离

Hook 的使用

可以使用 eslint-plugin-react-hooks 来减少 hooks 的使用错误

在 package.json 中:

1
2
3
4
5
6
7
8
9
"eslintConfig": {
"extends": "react-app",
"plugins": [
"react-hooks"
],
"rules":{
"react-hooks/rules-of-hooks": "error" // 出现hooks的使用错误,直接报错
}
}

useState

useState 用来替代之前类组件中 state 成员和 setState 方法的新的状态解决方案

直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component, useState } from 'react'
function App () {
const [count, setCount] = useState(0);
return (
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count})
</button>
);
}
export default App;

若需要从父组件中获取数据赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component, useState } from 'react'
function App (props) {
/*
const defaultCount = props.defaultCount || 0 // 这样使用会有问题,每次渲染都会运行,如果复杂度高会浪费过多资源
const [count, setCount] = useState(defaultCount);
*/
// 以下这样使用只在第一次渲染调用
const [count, setCount] = useState(() => {
return props.defaultCount || 0
});
return (
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count})
</button>
);
}
export default App;

useState 的使用主要注意两点:

  1. 使用需要规规矩矩,按顺序,次数不能多也不能少(使用 eslint-plugin-react-hooks 插件减少错误)
  2. 可以传入一个函数,实现延迟初始化提高效率

useEffect

useEffect 替代之前的副作用使用场景

副作用的时机(生命周期函数):

  • Mount 之后 - componentDidMount
  • Update 之后 - componentDidUpdate
  • Unmount 之前 - componentWillUnmount

类组件代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React, { Component } from 'react'
Class App2 extends Component {
state = {
count: 0,
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
}

// 这里最好使用类属性来声明函数,如果用类成员函数声明
// 则不能保证里面 this 指向的正确性,和严格一致的句柄
// 还可以只用下面更优雅的 装饰器 达到相同的目的
onResize = () => {
this.setState({
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
})
}
// @bind() 装饰器,还在查看期,未正式发布
// onResize() {}

componentDidMount() {
document.title = this.state.count
window.addEventListener('resize', this.onResize, false)
}

componentDidUpdate() {
document.title = this.state.count
}

componentWillUnmount() {
window.removeEventListener('resize', this.onResize, false)
}

render() {
const { count, size } = this.state
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count})
Size:{size.width} x {size.height}
</button>
}
}

使用 Hooks useEffect 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { Component, useState, useEffect } from 'react'
function App () {
const [count, setCount] = useState(0);
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})

// 现在没有Class函数也就不存在绑定 this 的问题了
onResize = () => {
setSize({
size: {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
})
}

// 会不断重复调用
useEffect(() => {
document.title = this.state.count
})

useEffect(() => {
// 绑定和解绑事件
window.addEventListener('resize', onResize, false)
return () => {
window.removeEventListener('resize', onResize, false)
}
}, []) // 注意这里传递空数组,可以让 useEffect 只调用一次,"[]"是第二个参数,数组的每一项都不变就会阻止 useEffect 重新运行
return (
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count})
Size:{size.width} x {size.height}
</button>
);
}
export default App;

之前类组件的生命周期函数在命名上比较好理解,但是其实都是围绕着组件的渲染和重渲染,useEffect 把它们抽象了一层,通过第二个参数来控制抽象的时机,这于生命周期函数是一样的。大家理解什么样的 useEffect 参数和什么周期函数是对应的,就可以灵活应用了。

Contenxt Hooks

类组件的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React, { Component, useState } from 'react'

// context
class Foo extends Component {
return() {
return (
<CountContext.Consumer>
{
count => <h1>{count}</h1>
}
</CountContext.Consumer>
)
}
}

// contextType
class Bar extends Component {
static contextType = CountContext
return() {
const count = this.context
return (
<h1>{count}</h1>
)
}
}

function App() {
const [count, setCount] = useState(0)

return (
<div>
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count})
</button>
<CountContext.provider value={count}>
<Foo/>
<Bar/>
</CountContext.provider>
</div>
);
}

useContext 的实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { Component, useState, useContext } from 'react'

function Counter() {
const count = useContext(CountContext)
return (
<h1>{count}</h1>
)
}

function App() {
const [count, setCount] = useState(0)

return (
<div>
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count})
</button>
<CountContext.provider value={count}>
<Counter/>
</CountContext.provider>
</div>
);
}

注意不要乱用 context,因为它会破坏组件的容积性

useMemo && useCallback

useMemo 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component, useState, useMemo } from 'react'

function Counter(props) {
return (
<h1>{props.count}</h1>
)
}

function App() {
const [count, setCount] = useState(0)

// useEffect 是在渲染后运行,useMemo是在渲染期间
const double = useMemo(() => {
return count * 2
}, [count]) // 改成 [count === 3] 测 试

return (
<div>
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count}), double:({double})
</button>
<Counter :count={count}/>
</div>
);
}

useCallback 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { Component, useState, useMemo, memo, useCallback } from 'react'

// memo 用于优化函数组件的渲染行为
// 当传入组件的属性值都不变的情况下,不会触发组件的重新渲染
const Counter = memo(function Counter(props) {
return (
<h1 onClick={props.onClick}>{props.count}</h1>
)
})

function App() {
const [count, setCount] = useState(0)
const [clickCount, setClickCount] = useState(0)

// useEffect 实在渲染后运行,useMemo是在渲染期间
const double = useMemo(() => {
return count * 2
}, [count === 3])

// useMemo 封装,防止 onClick 函数句柄每次变化导致组件更新,优化性能
/*
const onClick = useMemo(() => {
return () => {
console.log('Click')
}
}, [])
*/

// 等价于上面的 onClick
/*
const onClick = useCallback(() => {
console.log('Click')
}, [])
*/

/*
const onClick = useCallback(() => {
console.log('Click')
// setClickCount(clickCount + 1)
setClickCount((clickCount) => clickCount + 1) // 可以是一个函数
}, [clickCount])
*/

const onClick = useCallback(() => {
console.log('Click')
setClickCount((clickCount) => clickCount + 1) // 可以是一个函数,这样也不需要拿到 clickCount 的句柄了
}, [])

// 如果 useMemo 返回的是一个函数,那么可以直接用 useCallback 来省略顶层的函数,即 useMemo(() => fn) 等价于 useCallBack(fn)

return (
<div>
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count}), double:({double})
</button>
<Counter count={double} onClick={onClick}/>
</div>
);
}

所以 useCallback 是 useMemo 的变体

useRef

useRef 用于:

  • 获取子组件或者 DOM 节点的句柄
  • 渲染周期之间共享数据的存储(state等)

使用 useRef 之前,不能满足 clearInterval 定时器的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React, { Component, PureComponent, useState, useEffect, useMemo, memo, useCallback, useRef } from 'react'

// 函数组件不能通过 ref 获取到句柄
class Counter extends PureComponent {
speak () {
console.log(`now counter is: ${this.props.count}`)
}
render () {
const { props } = this
return (
<h1 onClick={props.onClick}>{props.count}</h1>
)
}
}

function App() {
const [count, setCount] = useState(0)
const [clickCount, setClickCount] = useState(0)
const counterRef = useRef()
let it // 定时器的句柄

// useEffect 实在渲染后运行,useMemo是在渲染期间
const double = useMemo(() => {
return count * 2
}, [count === 3])

const onClick = useCallback(() => {
console.log('Click')
setClickCount((clickCount) => clickCount + 1)

counterRef.current.speak() // 调用子组件的函数
}, [counterRef])

useEffect(() => {
it = setInterval(() => {
setCount(() => count + 1)
}, 1000)
}, [])

useEffect(() => {
if (count >= 10) {
clearInterval(it)
}
})

// 如果 useMemo 返回的是一个函数,那么可以直接用 useCallback 来省略顶层的函数,即 useMemo(() => fn) 等价于 useCallBack(fn)

return (
<div>
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count}), double:({double})
</button>
<Counter ref={counterRef} count={double} onClick={onClick}/>
</div>
);
}

使用 useRef:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React, { Component, PureComponent, useState, useEffect, useMemo, memo, useCallback, useRef } from 'react'

// 函数组件不能通过 ref 获取到句柄
class Counter extends PureComponent {
speak () {
console.log(`now counter is: ${this.props.count}`)
}
render () {
const { props } = this
return (
<h1 onClick={props.onClick}>{props.count}</h1>
)
}
}

function App() {
const [count, setCount] = useState(0)
const [clickCount, setClickCount] = useState(0)
const counterRef = useRef()
let it = useRef() // 定时器的句柄

// useEffect 实在渲染后运行,useMemo是在渲染期间
const double = useMemo(() => {
return count * 2
}, [count === 3])

const onClick = useCallback(() => {
console.log('Click')
setClickCount((clickCount) => clickCount + 1)

counterRef.current.speak() // 调用子组件的函数
}, [counterRef])

useEffect(() => {
it.current = setInterval(() => {
setCount(() => count + 1)
}, 1000)
}, [])

useEffect(() => {
if (count >= 10) {
clearInterval(it.current)
}
})

// 如果 useMemo 返回的是一个函数,那么可以直接用 useCallback 来省略顶层的函数,即 useMemo(() => fn) 等价于 useCallBack(fn)

return (
<div>
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count}), double:({double})
</button>
<Counter ref={counterRef} count={double} onClick={onClick}/>
</div>
);
}

童鞋们如果碰到需要在组件中获取上一次渲染的一些数据,甚至是 state,就把他们放到 useRef 中,下次渲染就能正确的获取到了。

上面用到了 useRef 的两种使用场景:

  1. 获取子组件或者 DOM 元素
  2. 同步不同渲染周期之间需要共享的数据

自定义 Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React, { Component, PureComponent, useState, useEffect, useRef } from 'react'

// 把 Counter 组件改造成 hook
// 证明可以返回 jsx 参与渲染
function useCounter(count) {
const size = useSize()
return (
<h1>{count},{size.width}x{size.height}</h1>
)
}

// 自定义 hook 函数必须以 use 开头
// 这里可以发现只有输入和输出的区别
function useCount(defaultCount) {
const [count, setCount] = useState(defaultCount)
let it = useRef() // 定时器的句柄

useEffect(() => {
it.current = setInterval(() => {
setCount(() => count + 1)
}, 1000)
}, [])

useEffect(() => {
if (count >= 10) {
clearInterval(it.current)
}
})
return [count, setCount]
}

// 假设很多个位置都需要获取浏览器窗口尺寸
// 写一个自定义 hook
function useSize() {
const [size, setSize] = setState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})

const onResize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}, [])

useEffet(() => {
window.addEventListener('resize', onResize, flase)

return () => {
window.removeEventListener('resize', onResize, flase)
}
})

return size
}

function App() {
const [count, setCount] = useCount(0)
const Counter = useCounter(count)
const size = useSize()

return (
<div>
<button
type="button"
onClick={() => {setCount(count + 1)}}
>
Click({count}),{size.width}x{size.height}
</button>
{Counter}
</div>
);
}

Hook 规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook。你可以:

  • 在 React 的函数组件中调用 Hook
  • 在自定义 Hook 中调用其他 Hook

遵循此规则,确保组件的状态逻辑在代码中清晰可见。